Passed
Branch wavefile-rw (0c075d)
by Rafael S.
02:29
created

WaveFile.setLabl_   A

Complexity

Conditions 3

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
dl 0
loc 13
rs 9.9
c 0
b 0
f 0
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFile class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import {encode, decode} from 'base64-arraybuffer-es6';
33
import WaveFileConverter from './lib/wavefile-converter';
34
import fixRIFFTag from './lib/fix-riff-tag';
35
36
/**
37
 * A class to manipulate wav files.
38
 */
39
export default class WaveFile extends WaveFileConverter {
40
41
  /**
42
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
43
   * @param {string} base64String A .wav file as a base64 string.
44
   * @throws {Error} If any property of the object appears invalid.
45
   */
46
  fromBase64(base64String) {
47
    this.fromBuffer(new Uint8Array(decode(base64String)));
48
  }
49
50
  /**
51
   * Return a base64 string representig the WaveFile object as a .wav file.
52
   * @return {string} A .wav file as a base64 string.
53
   * @throws {Error} If any property of the object appears invalid.
54
   */
55
  toBase64() {
56
    /** @type {!Uint8Array} */
57
    let buffer = this.toBuffer();
58
    return encode(buffer, 0, buffer.length);
59
  }
60
61
  /**
62
   * Return a DataURI string representig the WaveFile object as a .wav file.
63
   * The return of this method can be used to load the audio in browsers.
64
   * @return {string} A .wav file as a DataURI.
65
   * @throws {Error} If any property of the object appears invalid.
66
   */
67
  toDataURI() {
68
    return 'data:audio/wav;base64,' + this.toBase64();
69
  }
70
71
  /**
72
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
73
   * @param {string} dataURI A .wav file as DataURI.
74
   * @throws {Error} If any property of the object appears invalid.
75
   */
76
  fromDataURI(dataURI) {
77
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
78
  }
79
80
  /**
81
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
82
   * then it is created. It if exists, it is overwritten.
83
   * @param {string} tag The tag name.
84
   * @param {string} value The tag value.
85
   * @throws {Error} If the tag name is not valid.
86
   */
87
  setTag(tag, value) {
88
    tag = fixRIFFTag(tag);
89
    /** @type {!Object} */
90
    let index = this.getTagIndex_(tag);
91
    if (index.TAG !== null) {
92
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
93
        value.length + 1;
94
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
95
    } else if (index.LIST !== null) {
96
      this.LIST[index.LIST].subChunks.push({
97
        chunkId: tag,
98
        chunkSize: value.length + 1,
99
        value: value});
100
    } else {
101
      this.LIST.push({
102
        chunkId: 'LIST',
103
        chunkSize: 8 + value.length + 1,
104
        format: 'INFO',
105
        subChunks: []});
106
      this.LIST[this.LIST.length - 1].subChunks.push({
107
        chunkId: tag,
108
        chunkSize: value.length + 1,
109
        value: value});
110
    }
111
  }
112
113
  /**
114
   * Return the value of a RIFF tag in the INFO chunk.
115
   * @param {string} tag The tag name.
116
   * @return {?string} The value if the tag is found, null otherwise.
117
   */
118
  getTag(tag) {
119
    /** @type {!Object} */
120
    let index = this.getTagIndex_(tag);
121
    if (index.TAG !== null) {
122
      return this.LIST[index.LIST].subChunks[index.TAG].value;
123
    }
124
    return null;
125
  }
126
127
  /**
128
   * Return a Object<tag, value> with the RIFF tags in the file.
129
   * @return {!Object<string, string>} The file tags.
130
   */
131
  listTags() {
132
    /** @type {?number} */
133
    let index = this.getLISTINFOIndex_();
134
    /** @type {!Object} */
135
    let tags = {};
136
    if (index !== null) {
137
      for (let i = 0, len = this.LIST[index].subChunks.length; i < len; i++) {
138
        tags[this.LIST[index].subChunks[i].chunkId] =
139
          this.LIST[index].subChunks[i].value;
140
      }
141
    }
142
    return tags;
143
  }
144
145
  /**
146
   * Remove a RIFF tag from the INFO chunk.
147
   * @param {string} tag The tag name.
148
   * @return {boolean} True if a tag was deleted.
149
   */
150
  deleteTag(tag) {
151
    /** @type {!Object} */
152
    let index = this.getTagIndex_(tag);
153
    if (index.TAG !== null) {
154
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
155
      return true;
156
    }
157
    return false;
158
  }
159
160
  /**
161
   * Create a cue point in the wave file.
162
   * @param {number} position The cue point position in milliseconds.
163
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
164
   */
165
  setCuePoint(position, labl='') {
166
    this.cue.chunkId = 'cue ';
167
    position = (position * this.fmt.sampleRate) / 1000;
168
    /** @type {!Array<!Object>} */
169
    let existingPoints = this.getCuePoints_();
170
    this.clearLISTadtl_();
171
    /** @type {number} */
172
    let len = this.cue.points.length;
173
    this.cue.points = [];
174
    /** @type {boolean} */
175
    let hasSet = false;
176
    if (len === 0) {
177
      this.setCuePoint_(position, 1, labl);
178
    } else {
179
      for (let i = 0; i < len; i++) {
180
        if (existingPoints[i].dwPosition > position && !hasSet) {
181
          this.setCuePoint_(position, i + 1, labl);
182
          this.setCuePoint_(
183
            existingPoints[i].dwPosition,
184
            i + 2,
185
            existingPoints[i].label);
186
          hasSet = true;
187
        } else {
188
          this.setCuePoint_(
189
            existingPoints[i].dwPosition,
190
            i + 1,
191
            existingPoints[i].label);
192
        }
193
      }
194
      if (!hasSet) {
195
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
196
      }
197
    }
198
    this.cue.dwCuePoints = this.cue.points.length;
199
  }
200
201
  /**
202
   * Remove a cue point from a wave file.
203
   * @param {number} index the index of the point. First is 1,
204
   *    second is 2, and so on.
205
   */
206
  deleteCuePoint(index) {
207
    this.cue.chunkId = 'cue ';
208
    /** @type {!Array<!Object>} */
209
    let existingPoints = this.getCuePoints_();
210
    this.clearLISTadtl_();
211
    /** @type {number} */
212
    let len = this.cue.points.length;
213
    this.cue.points = [];
214
    for (let i = 0; i < len; i++) {
215
      if (i + 1 !== index) {
216
        this.setCuePoint_(
217
          existingPoints[i].dwPosition,
218
          i + 1,
219
          existingPoints[i].label);
220
      }
221
    }
222
    this.cue.dwCuePoints = this.cue.points.length;
223
    if (this.cue.dwCuePoints) {
224
      this.cue.chunkId = 'cue ';
225
    } else {
226
      this.cue.chunkId = '';
227
      this.clearLISTadtl_();
228
    }
229
  }
230
231
  /**
232
   * Return an array with all cue points in the file, in the order they appear
233
   * in the file.
234
   * The difference between this method and using the list in WaveFile.cue
235
   * is that the return value of this method includes the position in
236
   * milliseconds of each cue point (WaveFile.cue only have the sample offset)
237
   * @return {!Array<!Object>}
238
   */
239
  listCuePoints() {
240
    /** @type {!Array<!Object>} */
241
    let points = this.getCuePoints_();
242
    for (let i = 0, len = points.length; i < len; i++) {
243
      points[i].milliseconds =
244
        (points[i].dwPosition / this.fmt.sampleRate) * 1000;
245
    }
246
    return points;
247
  }
248
249
  /**
250
   * Update the label of a cue point.
251
   * @param {number} pointIndex The ID of the cue point.
252
   * @param {string} label The new text for the label.
253
   */
254
  updateLabel(pointIndex, label) {
255
    /** @type {?number} */
256
    let cIndex = this.getAdtlChunk_();
257
    if (cIndex !== null) {
258
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
259
        if (this.LIST[cIndex].subChunks[i].dwName ==
260
            pointIndex) {
261
          this.LIST[cIndex].subChunks[i].value = label;
262
        }
263
      }
264
    }
265
  }
266
267
  /**
268
   * Push a new cue point in this.cue.points.
269
   * @param {number} position The position in milliseconds.
270
   * @param {number} dwName the dwName of the cue point
271
   * @private
272
   */
273
  setCuePoint_(position, dwName, label) {
274
    this.cue.points.push({
275
      dwName: dwName,
276
      dwPosition: position,
277
      fccChunk: 'data',
278
      dwChunkStart: 0,
279
      dwBlockStart: 0,
280
      dwSampleOffset: position,
281
    });
282
    this.setLabl_(dwName, label);
283
  }
284
285
  /**
286
   * Return an array with all cue points in the file, in the order they appear
287
   * in the file.
288
   * @return {!Array<!Object>}
289
   * @private
290
   */
291
  getCuePoints_() {
292
    /** @type {!Array<!Object>} */
293
    let points = [];
294
    for (let i = 0, len = this.cue.points.length; i < len; i++) {
295
      points.push({
296
        dwPosition: this.cue.points[i].dwPosition,
297
        label: this.getLabelForCuePoint_(
298
          this.cue.points[i].dwName)});
299
    }
300
    return points;
301
  }
302
303
  /**
304
   * Return the label of a cue point.
305
   * @param {number} pointDwName The ID of the cue point.
306
   * @return {string}
307
   * @private
308
   */
309
  getLabelForCuePoint_(pointDwName) {
310
    /** @type {?number} */
311
    let cIndex = this.getAdtlChunk_();
312
    if (cIndex !== null) {
313
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
314
        if (this.LIST[cIndex].subChunks[i].dwName ==
315
            pointDwName) {
316
          return this.LIST[cIndex].subChunks[i].value;
317
        }
318
      }
319
    }
320
    return '';
321
  }
322
323
  /**
324
   * Clear any LIST chunk labeled as 'adtl'.
325
   * @private
326
   */
327
  clearLISTadtl_() {
328
    for (let i = 0, len = this.LIST.length; i < len; i++) {
329
      if (this.LIST[i].format == 'adtl') {
330
        this.LIST.splice(i);
331
      }
332
    }
333
  }
334
335
  /**
336
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
337
   * @param {number} dwName The ID of the cue point.
338
   * @param {string} label The label for the cue point.
339
   * @private
340
   */
341
  setLabl_(dwName, label) {
342
    /** @type {?number} */
343
    let adtlIndex = this.getAdtlChunk_();
344
    if (adtlIndex === null) {
345
      this.LIST.push({
346
        chunkId: 'LIST',
347
        chunkSize: 4,
348
        format: 'adtl',
349
        subChunks: []});
350
      adtlIndex = this.LIST.length - 1;
351
    }
352
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
353
  }
354
355
  /**
356
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
357
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
358
   * @param {number} dwName The ID of the cue point.
359
   * @param {string} label The label for the cue point.
360
   * @private
361
   */
362
  setLabelText_(adtlIndex, dwName, label) {
363
    this.LIST[adtlIndex].subChunks.push({
364
      chunkId: 'labl',
365
      chunkSize: label.length,
366
      dwName: dwName,
367
      value: label
368
    });
369
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
370
  }
371
372
  /**
373
   * Return the index of the 'adtl' LIST in this.LIST.
374
   * @return {?number}
375
   * @private
376
   */
377
  getAdtlChunk_() {
378
    for (let i = 0, len = this.LIST.length; i < len; i++) {
379
      if (this.LIST[i].format == 'adtl') {
380
        return i;
381
      }
382
    }
383
    return null;
384
  }
385
386
  /**
387
   * Return the index of the INFO chunk in the LIST chunk.
388
   * @return {?number} the index of the INFO chunk.
389
   * @private
390
   */
391
  getLISTINFOIndex_() {
392
    /** @type {?number} */
393
    let index = null;
394
    for (let i = 0, len = this.LIST.length; i < len; i++) {
395
      if (this.LIST[i].format === 'INFO') {
396
        index = i;
397
        break;
398
      }
399
    }
400
    return index;
401
  }
402
403
  /**
404
   * Return the index of a tag in a FILE chunk.
405
   * @param {string} tag The tag name.
406
   * @return {!Object<string, ?number>}
407
   *    Object.LIST is the INFO index in LIST
408
   *    Object.TAG is the tag index in the INFO
409
   * @private
410
   */
411
  getTagIndex_(tag) {
412
    /** @type {!Object<string, ?number>} */
413
    let index = {LIST: null, TAG: null};
414
    for (let i = 0, len = this.LIST.length; i < len; i++) {
415
      if (this.LIST[i].format == 'INFO') {
416
        index.LIST = i;
417
        for (let j=0, subLen = this.LIST[i].subChunks.length; j < subLen; j++) {
418
          if (this.LIST[i].subChunks[j].chunkId == tag) {
419
            index.TAG = j;
420
            break;
421
          }
422
        }
423
        break;
424
      }
425
    }
426
    return index;
427
  }
428
}
429